﻿//  Symplectic Spark
//  Copyright 2010 Symplectic Ltd
//  Created by Martyn Whitwell (martyn@symplectic.co.uk)

//  This file is part of Spark.

//  Spark is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.

//  Spark is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.

//  You should have received a copy of the GNU General Public License
//  along with Spark.  If not, see <http://www.gnu.org/licenses/>.

// $URL: https://symplectic-spark.googlecode.com/svn/trunk/website/App_Code/Component.cs $
// $LastChangedDate: 2010-06-09 11:38:27 +0100 (Wed, 09 Jun 2010) $
// $LastChangedRevision: 52 $
// $LastChangedBy: martyn@symplectic.co.uk $

using System;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Xsl;
using System.Xml.XPath;
using System.IO;
using System.Web;
using System.Text;
using System.Collections.Generic;


/// <summary>
/// Summary description for Component
/// </summary>
namespace Symplectic.Spark
{
    public class Component : BaseType
    {
        #region Logger
        private static readonly log4net.ILog Logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
        #endregion

        public ComponentMode ComponentMode { get; private set; }

        private string CacheValue { get; set; }
        private DateTime CacheExpiration { get; set; }
        private int CacheDuration { get; set; }

        private XElement Value { get; set; }
        private string Xsl { get; set; }
        private string Uri { get; set; }
        private DynamicElementInvoker DynamicElementInvoker { get; set; }


        public Component(string name, XElement value, ComponentMode componentMode)
        {
            CacheDuration = int.MinValue;
            Name = name;
            Value = value;
            ComponentMode = componentMode;
        }

       

        public Component(XElement component)
        {
            if (component.Elements().Count() > 0)
                InitialiseFromXml(readAttributeString(component, "name"), component.Elements().First());
            else
                throw new Exception("Could not parse " + component.ToString());
        }

        public Component(string name, XElement value)
        {
            InitialiseFromXml(name, value);
        }

        private void InitialiseFromXml(string name, XElement value)
        {
            Name = name;
            Value = value;
            CacheDuration = readAttributeInteger(Value, "cache", true);

            switch (Value.Name.LocalName)
            {
                case "api":
                    ComponentMode = ComponentMode.API;
                    break;
                case "choose":
                    ComponentMode = ComponentMode.Choose;
                    break;
                case "dynamic-code":
                    ComponentMode = ComponentMode.DynamicCode;
                    break;
                case "if":
                    ComponentMode = ComponentMode.If;
                    break;
                case "internal-function":
                    ComponentMode = ComponentMode.InternalFunction;
                    break;
                case "value":
                    ComponentMode = ComponentMode.Value;
                    break;
                case "xml":
                    ComponentMode = ComponentMode.XML;
                    break;
                case "hacked-user-feed":
                    ComponentMode = ComponentMode.HackedUserFeed;
                    break;
                case "buffered":
                    ComponentMode = ComponentMode.Buffered;
                    break;
                default:
                    throw new Exception("Could not parse type '" + Value.Name.LocalName + "' in (" + Value.ToString() + ")");
            }

            //Xsl
            Xsl = readAttributeString(Value, "xsl", true);

            //Only API component type can use use ApiBaseUri attribute
            if (ComponentMode == ComponentMode.API || ComponentMode == ComponentMode.HackedUserFeed)
            {
                string apiBaseUri = readAttributeString(Value, "api-base-uri", true);
                string relativeUri = readAttributeString(Value, "relative-uri", true);
                if (apiBaseUri != null)
                {
                    Uri = apiBaseUri;
                    if (relativeUri != null)
                        Uri += relativeUri;
                }
            }
            //Only read Uri if it has not already been set above
            if (Uri == null)
                Uri = readAttributeString(Value, "uri", true);
        }

        public string GetValue(RequestSpec requestSpec)
        {
            if (CacheDuration > 0 && CacheExpiration != null && CacheValue != null && CacheExpiration > DateTime.Now)
            {
                //Logger.DebugFormat("RETURNING FROM CACHE for {0}", Name);
                return CacheValue;
            }
            else
            {
                XDocument xmldata = null;
                switch (ComponentMode)
                {
                    case ComponentMode.API:
                    case ComponentMode.XML:
                        if (Uri != null)
                        {
                            Uri uriWithParams = new Uri(Uri + GenerateUrlParameters(Value.Elements("parameter"), requestSpec));
                            try
                            {
                                xmldata = XDocument.Parse(GetRequest(uriWithParams, Global.GetRequestTimeout));
                                CacheValue = Transform(xmldata, requestSpec);
                            }
                            catch (System.Net.WebException webex)
                            {
                                return "[Error: " + webex.Message + " for " + uriWithParams.ToString() + "]";
                            }
                        }
                        else //no URI specified, use inner content instead
                        {
                            xmldata = XDocument.Load(Value.CreateReader());
                            CacheValue = Transform(xmldata, requestSpec);
                        }
                        break;

                    case ComponentMode.DynamicCode:
                        if (DynamicElementInvoker == null)
                            DynamicElementInvoker = new DynamicElementInvoker(Value.Value);

                        if (DynamicElementInvoker.DynamicElement != null)
                            try
                            {
                                CacheValue = DynamicElementInvoker.Invoke();
                            }
                            catch (Exception ex)
                            {
                                return "[Error: " + ex.Message + " when invoking dynamic element " + Name + "]";
                            }
                        else
                            return DynamicElementInvoker.Errors;
                        break;

                    case ComponentMode.If:
                    case ComponentMode.Choose:
                        XElement chosenValue = Expression.Evaluate(Value, requestSpec);
                        if (chosenValue == null)
                            return null;
                        else
                            CacheValue = new Component(Name, chosenValue).GetValue(requestSpec);
                        break;

                    case ComponentMode.InternalFunction:
                        return InternalFunctions.GetValue(readAttributeString(Value, "name"), requestSpec);

                    case ComponentMode.Value:
                        //check if it is a composite value (i.e. refering to other components)
                        if (Value.ToString().Contains("[[") || Value.ToString().Contains("((") || Value.ToString().Contains("{{"))
                            CacheValue = new Templater(requestSpec, new Template(PrintInnerNodes(Value))).Content; //construct dynamic template and execute
                        else
                            CacheValue = PrintInnerNodes(Value);
                        break;

                    case ComponentMode.HackedUserFeed:
                        HackedUserFeed huf = new HackedUserFeed(Global.IndexDirectory);
                        xmldata = huf.Find(Expression.GetOperandValue("((query))", requestSpec),
                            int.Parse(Expression.GetOperandValue("((per-page))", requestSpec)),
                            int.Parse(Expression.GetOperandValue("((page))", requestSpec)));
                        CacheValue = Transform(xmldata, requestSpec);
                        break;

                    case ComponentMode.Buffered:
                        string bufferFilename = Path.Combine(Global.BufferFolderAbsolute, GenerateUrlParameters(Value.Elements("parameter"), requestSpec));

                        if (File.Exists(bufferFilename))
                        {
                            xmldata = XDocument.Load(bufferFilename);
                            CacheValue = Transform(xmldata, requestSpec);
                        }
                        else
                        {
                            return "[Buffered data missing - " + bufferFilename + " - please try again later]";
                        }
                        break;

                    default:
                        return "[no value defined for " + ComponentMode.ToString() + "]";
                }

                if (CacheDuration > 0)
                {
                    CacheExpiration = DateTime.Now.AddSeconds(CacheDuration);
                    //Logger.DebugFormat("Adding {0} to Cache with Expiration {1}", Name, CacheExpiration);
                }
                return CacheValue;
            }
        }

       
        private string Transform(XDocument data, RequestSpec requestSpec)
        {
            if (Xsl == null)
                return data.ToString();
            else
            {
                //XDocument output = new XDocument();
                //using (XmlWriter writer = output.CreateWriter())
                StringBuilder output = new StringBuilder();
                
                using(StringWriter writer = new StringWriter(output))
                {
                    // Load the style sheet.
                    XslCompiledTransform xslt = new XslCompiledTransform();
					XsltSettings settings = new XsltSettings(true, false);
                    xslt.Load(Path.Combine(Global.XslPathAbsolute, Xsl), settings, new XmlUrlResolver());
                    XsltArgumentList xargs = new XsltArgumentList();
                    xargs.AddParam("application-path", String.Empty, Global.ApplicationPathVirtual);
                    xargs.AddParam("url", String.Empty, requestSpec.Request.Url.AbsolutePath);

                    foreach (string key in requestSpec.AllKeys)
                    {
                        if (key != null && key != String.Empty && requestSpec[key] != null)
                            xargs.AddParam(key, String.Empty, HttpUtility.UrlEncode(requestSpec[key]));
                    }
                                        
                    try
                    {
                        xslt.Transform(data.CreateNavigator(), xargs, writer);
                    }
                    catch (XsltException ex)
                    {
                        throw new Exception("Failed to process XSL: " + Xsl + " Line number:" + ex.LineNumber.ToString() + " position: " + ex.LinePosition.ToString() + " message " + ex.Message, ex);
                    }
                }
                return output.ToString();
            }
        }
        

        private string GetRequest(Uri uri, int timeoutMilliseconds)
        {
            var request = System.Net.WebRequest.Create(uri);
            request.Timeout = timeoutMilliseconds;
            using (var response = request.GetResponse())
            using (var stream = response.GetResponseStream())
            using (var reader = new System.IO.StreamReader(stream))
            {
                return reader.ReadToEnd();
            }
        }

        private string GenerateUrlParameters(IEnumerable<XElement> parameters, RequestSpec requestSpec)
        {
            StringBuilder prequery = new StringBuilder("");
            StringBuilder result = new StringBuilder("");
            bool ampersand = false;
            foreach (XElement parameter in parameters)
            {
                //Append without URL-encoding, automatically add trailing forward-slash /
                if (readAttributeString(parameter, "type") == "url-folder-forward-slash" && readAttributeString(parameter, "value") != null)
                {
                    prequery.Append(Expression.GetOperandValue(readAttributeString(parameter, "value"), requestSpec) + "/");
                }

                //Append without URL-encoding, automatically add trailing backward-slash /
                if (readAttributeString(parameter, "type") == "url-folder-backward-slash" && readAttributeString(parameter, "value") != null)
                {
                    prequery.Append(Expression.GetOperandValue(readAttributeString(parameter, "value"), requestSpec) + @"\");
                }


                if (readAttributeString(parameter, "type") == "url-component" && readAttributeString(parameter, "value") != null)
                {
                    prequery.Append(HttpUtility.UrlEncode(
                            Expression.GetOperandValue(readAttributeString(parameter, "value"), requestSpec)
                            ));
                }

                if (readAttributeString(parameter, "type") == "url-query" && readAttributeString(parameter, "name") != null)
                {
                    if (ampersand)
                        result.Append("&");
                    result.Append(HttpUtility.UrlEncode(readAttributeString(parameter, "name")) + "=");

                    if (readAttributeString(parameter, "value") != null)
                        result.Append(HttpUtility.UrlEncode(
                            Expression.GetOperandValue(readAttributeString(parameter, "value"), requestSpec)
                            ));
                    ampersand = true;
                }

                //Only add to query string if value is not null
                if (readAttributeString(parameter, "type") == "url-optional-query" && readAttributeString(parameter, "name") != null)
                {
                    string value = null;
                    if (readAttributeString(parameter, "value") != null)
                    {
                        value = Expression.GetOperandValue(readAttributeString(parameter, "value"), requestSpec);
                        if (value != null)
                        {
                            if (ampersand)
                                result.Append("&");
                            result.Append(HttpUtility.UrlEncode(readAttributeString(parameter, "name")) + "=");
                            result.Append(HttpUtility.UrlEncode(value));
                            ampersand = true;
                        }
                    }
                }
                
            }
            if (result.Length > 0)
                return prequery.ToString() + "?" + result.ToString();
            else
                return prequery.ToString();
        }
        
        private bool EvaluateCondition(string condition)
        {
            //if condition.
            return false;
        }
        
        private string PrintInnerNodes(XElement element)
        {
            return element.Nodes().Select(n => n.ToString(SaveOptions.DisableFormatting)).DefaultIfEmpty().Aggregate((a, b) => a + b);
        }

    }
}

